Passed
Branch v10.2.x (85bd8e)
by Rafael S.
02:37
created

WaveFileMetaEditor.setLabelText_   A

Complexity

Conditions 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 9
rs 10
c 0
b 0
f 0
cc 1
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFileMetaEditor class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
import { WaveFileCreator } from './wavefile-creator';
31
32
/**
33
 * A class to edit meta information in wav files.
34
 * @extends WaveFileCreator
35
 * @ignore
36
 */
37
export class WaveFileMetaEditor extends WaveFileCreator {
38
39
  /**
40
   * Return the value of a RIFF tag in the INFO chunk.
41
   * @param {string} tag The tag name.
42
   * @return {?string} The value if the tag is found, null otherwise.
43
   */
44
  getTag(tag) {
45
    /** @type {!Object} */
46
    let index = this.getTagIndex_(tag);
47
    if (index.TAG !== null) {
48
      return this.LIST[index.LIST].subChunks[index.TAG].value;
49
    }
50
    return null;
51
  }
52
53
  /**
54
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
55
   * then it is created. It if exists, it is overwritten.
56
   * @param {string} tag The tag name.
57
   * @param {string} value The tag value.
58
   * @throws {Error} If the tag name is not valid.
59
   */
60
  setTag(tag, value) {
61
    tag = fixRIFFTag_(tag);
62
    /** @type {!Object} */
63
    let index = this.getTagIndex_(tag);
64
    if (index.TAG !== null) {
65
      this.LIST[index.LIST].subChunks[index.TAG].chunkSize =
66
        value.length + 1;
67
      this.LIST[index.LIST].subChunks[index.TAG].value = value;
68
    } else if (index.LIST !== null) {
69
      this.LIST[index.LIST].subChunks.push({
70
        chunkId: tag,
71
        chunkSize: value.length + 1,
72
        value: value});
73
    } else {
74
      this.LIST.push({
75
        chunkId: 'LIST',
76
        chunkSize: 8 + value.length + 1,
77
        format: 'INFO',
78
        subChunks: []});
79
      this.LIST[this.LIST.length - 1].subChunks.push({
80
        chunkId: tag,
81
        chunkSize: value.length + 1,
82
        value: value});
83
    }
84
  }
85
86
  /**
87
   * Remove a RIFF tag from the INFO chunk.
88
   * @param {string} tag The tag name.
89
   * @return {boolean} True if a tag was deleted.
90
   */
91
  deleteTag(tag) {
92
    /** @type {!Object} */
93
    let index = this.getTagIndex_(tag);
94
    if (index.TAG !== null) {
95
      this.LIST[index.LIST].subChunks.splice(index.TAG, 1);
96
      return true;
97
    }
98
    return false;
99
  }
100
101
  /**
102
   * Return a Object<tag, value> with the RIFF tags in the file.
103
   * @return {!Object<string, string>} The file tags.
104
   */
105
  listTags() {
106
    /** @type {?number} */
107
    let index = this.getLISTINFOIndex_();
108
    /** @type {!Object} */
109
    let tags = {};
110
    if (index !== null) {
111
      for (let i = 0, len = this.LIST[index].subChunks.length; i < len; i++) {
112
        tags[this.LIST[index].subChunks[i].chunkId] =
113
          this.LIST[index].subChunks[i].value;
114
      }
115
    }
116
    return tags;
117
  }
118
119
  /**
120
   * Return an array with all cue points in the file, in the order they appear
121
   * in the file.
122
   * Objects representing cue points/regions look like this:
123
   *   {
124
   *     position: 500, // the position in milliseconds
125
   *     label: 'cue marker 1',
126
   *     end: 1500, // the end position in milliseconds
127
   *     dwName: 1,
128
   *     dwPosition: 0,
129
   *     fccChunk: 'data',
130
   *     dwChunkStart: 0,
131
   *     dwBlockStart: 0,
132
   *     dwSampleOffset: 22050, // the position as a sample offset
133
   *     dwSampleLength: 3646827, // length as a sample count, 0 if not a region
134
   *     dwPurposeID: 544106354,
135
   *     dwCountry: 0,
136
   *     dwLanguage: 0,
137
   *     dwDialect: 0,
138
   *     dwCodePage: 0,
139
   *   }
140
   * @return {!Array<Object>}
141
   */
142
  listCuePoints() {
143
    /** @type {!Array<!Object>} */
144
    let points = this.getCuePoints_();
145
    for (let i = 0, len = points.length; i < len; i++) {
146
147
      // Add attrs that should exist in the object
148
      points[i].position =
149
        (points[i].dwSampleOffset / this.fmt.sampleRate) * 1000;
150
151
      // If it is a region, calc the end
152
      // position in milliseconds
153
      if (points[i].dwSampleLength) {
154
        points[i].end =
155
          (points[i].dwSampleLength / this.fmt.sampleRate) * 1000;
156
        points[i].end += points[i].position;
157
      // If its not a region, end should be null
158
      } else {
159
        points[i].end = null;
160
      }
161
162
      // Remove attrs that should not go in the results
163
      delete points[i].value;
164
    }
165
    return points;
166
  }
167
168
  /**
169
   * Create a cue point in the wave file.
170
   * @param {!{
171
   *   position: number,
172
   *   label: ?string,
173
   *   end: ?number,
174
   *   dwPurposeID: ?number,
175
   *   dwCountry: ?number,
176
   *   dwLanguage: ?number,
177
   *   dwDialect: ?number,
178
   *   dwCodePage: ?number
179
   * }} pointData A object with the data of the cue point.
180
   *
181
   * # Only required attribute to create a cue point:
182
   * pointData.position: The position of the point in milliseconds
183
   *
184
   * # Optional attribute for cue points:
185
   * pointData.label: A string label for the cue point
186
   *
187
   * # Extra data used for regions
188
   * pointData.end: A number representing the end of the region,
189
   *   in milliseconds, counting from the start of the file. If
190
   *   no end attr is specified then no region is created.
191
   *
192
   * # You may also specify the following attrs for regions, all optional:
193
   * pointData.dwPurposeID
194
   * pointData.dwCountry
195
   * pointData.dwLanguage
196
   * pointData.dwDialect
197
   * pointData.dwCodePage
198
   */
199
  setCuePoint(pointData) {
200
    this.cue.chunkId = 'cue ';
201
202
    // label attr should always exist
203
    if (!pointData.label) {
204
      pointData.label = '';
205
    }
206
207
    /**
208
     * Load the existing points before erasing
209
     * the LIST 'adtl' chunk and the cue attr
210
     * @type {!Array<!Object>}
211
     */
212
    let existingPoints = this.getCuePoints_();
213
214
    // Clear any LIST labeled 'adtl'
215
    // The LIST chunk should be re-written
216
    // after the new cue point is created
217
    this.clearLISTadtl_();
218
219
    // Erase this.cue so it can be re-written
220
    // after the point is added
221
    this.cue.points = [];
222
223
    /**
224
     * Cue position param is informed in milliseconds,
225
     * here its value is converted to the sample offset
226
     * @type {number}
227
     */
228
    pointData.dwSampleOffset =
229
      (pointData.position * this.fmt.sampleRate) / 1000;
230
    /**
231
     * end param is informed in milliseconds, counting
232
     * from the start of the file.
233
     * here its value is converted to the sample length
234
     * of the region.
235
     * @type {number}
236
     */
237
    pointData.dwSampleLength = 0;
238
    if (pointData.end) {
239
      pointData.dwSampleLength = 
240
        ((pointData.end * this.fmt.sampleRate) / 1000) -
241
        pointData.dwSampleOffset;
242
    }
243
244
    // If there were no cue points in the file,
245
    // insert the new cue point as the first
246
    if (existingPoints.length === 0) {
247
      this.setCuePoint_(pointData, 1);
248
249
    // If the file already had cue points, This new one
250
    // must be added in the list according to its position.
251
    } else {
252
      this.setCuePointInOrder_(existingPoints, pointData);
253
    }
254
    this.cue.dwCuePoints = this.cue.points.length;
255
  }
256
257
  /**
258
   * Remove a cue point from a wave file.
259
   * @param {number} index the index of the point. First is 1,
260
   *    second is 2, and so on.
261
   */
262
  deleteCuePoint(index) {
263
    this.cue.chunkId = 'cue ';
264
    /** @type {!Array<!Object>} */
265
    let existingPoints = this.getCuePoints_();
266
    this.clearLISTadtl_();
267
    /** @type {number} */
268
    let len = this.cue.points.length;
269
    this.cue.points = [];
270
    for (let i = 0; i < len; i++) {
271
      if (i + 1 !== index) {
272
        this.setCuePoint_(existingPoints[i], i + 1);
273
      }
274
    }
275
    this.cue.dwCuePoints = this.cue.points.length;
276
    if (this.cue.dwCuePoints) {
277
      this.cue.chunkId = 'cue ';
278
    } else {
279
      this.cue.chunkId = '';
280
      this.clearLISTadtl_();
281
    }
282
  }
283
284
  /**
285
   * Update the label of a cue point.
286
   * @param {number} pointIndex The ID of the cue point.
287
   * @param {string} label The new text for the label.
288
   */
289
  updateLabel(pointIndex, label) {
290
    /** @type {?number} */
291
    let cIndex = this.getAdtlChunk_();
292
    if (cIndex !== null) {
293
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
294
        if (this.LIST[cIndex].subChunks[i].dwName ==
295
            pointIndex) {
296
          this.LIST[cIndex].subChunks[i].value = label;
297
        }
298
      }
299
    }
300
  }
301
302
  /**
303
   * Return an array with all cue points in the file, in the order they appear
304
   * in the file.
305
   * @return {!Array<!Object>}
306
   * @private
307
   */
308
  getCuePoints_() {
309
    /** @type {!Array<!Object>} */
310
    let points = [];
311
    for (let i = 0; i < this.cue.points.length; i++) {
312
      /** @type {!Object} */
313
      let chunk = this.cue.points[i];
314
      /** @type {!Object} */
315
      let pointData = this.getDataForCuePoint_(chunk.dwName);
316
      pointData.label = pointData.value ? pointData.value : '';
317
      pointData.dwPosition = chunk.dwPosition;
318
      pointData.fccChunk = chunk.fccChunk;
319
      pointData.dwChunkStart = chunk.dwChunkStart;
320
      pointData.dwBlockStart = chunk.dwBlockStart;
321
      pointData.dwSampleOffset = chunk.dwSampleOffset;
322
      points.push(pointData);
323
    }
324
    return points;
325
  }
326
327
  /**
328
   * Return the associated data of a cue point.
329
   * @param {number} pointDwName The ID of the cue point.
330
   * @return {!Object}
331
   * @private
332
   */
333
  getDataForCuePoint_(pointDwName) {
334
    /** @type {?number} */
335
    let cIndex = this.getAdtlChunk_();
336
    /** @type {!Object} */
337
    let pointData = {};
338
    if (cIndex !== null) {
339
      // got through all chunks in the adtl LIST checking
340
      // for references to this cue point
341
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
342
        if (this.LIST[cIndex].subChunks[i].dwName ==
343
            pointDwName && this.LIST[cIndex].subChunks[i].chunkId) {
344
          /** @type {!Object} */
345
          let chunk = this.LIST[cIndex].subChunks[i];
346
          // Some chunks may reference the point but
347
          // have a empty text; this is to ensure that if
348
          // one chunk that reference the point has a text,
349
          // this value will be kept as the associated data label
350
          // for the cue point.
351
          // If different values are present, the last value found
352
          // will be considered the label for the cue point.
353
          pointData.value = chunk.value ? chunk.value : pointData.value;
354
          pointData.dwName = chunk.dwName ? chunk.dwName : 0;
355
          pointData.dwSampleLength =
356
            chunk.dwSampleLength ? chunk.dwSampleLength : 0;
357
          pointData.dwPurposeID = chunk.dwPurposeID ? chunk.dwPurposeID : 0;
358
          pointData.dwCountry = chunk.dwCountry ? chunk.dwCountry : 0;
359
          pointData.dwLanguage = chunk.dwLanguage ? chunk.dwLanguage : 0;
360
          pointData.dwDialect = chunk.dwDialect ? chunk.dwDialect : 0;
361
          pointData.dwCodePage = chunk.dwCodePage ? chunk.dwCodePage : 0;
362
        }
363
      }
364
    }
365
    return pointData;
366
  }
367
368
  /**
369
   * Return the index of the INFO chunk in the LIST chunk.
370
   * @return {?number} the index of the INFO chunk.
371
   * @private
372
   */
373
  getLISTINFOIndex_() {
374
    /** @type {?number} */
375
    let index = null;
376
    for (let i = 0, len = this.LIST.length; i < len; i++) {
377
      if (this.LIST[i].format === 'INFO') {
378
        index = i;
379
        break;
380
      }
381
    }
382
    return index;
383
  }
384
385
  /**
386
   * Return the index of the 'adtl' LIST in this.LIST.
387
   * @return {?number}
388
   * @private
389
   */
390
  getAdtlChunk_() {
391
    for (let i = 0, len = this.LIST.length; i < len; i++) {
392
      if (this.LIST[i].format == 'adtl') {
393
        return i;
394
      }
395
    }
396
    return null;
397
  }
398
399
  /**
400
   * Return the index of a tag in a FILE chunk.
401
   * @param {string} tag The tag name.
402
   * @return {!Object<string, ?number>}
403
   *    Object.LIST is the INFO index in LIST
404
   *    Object.TAG is the tag index in the INFO
405
   * @private
406
   */
407
  getTagIndex_(tag) {
408
    /** @type {!Object<string, ?number>} */
409
    let index = {LIST: null, TAG: null};
410
    for (let i = 0, len = this.LIST.length; i < len; i++) {
411
      if (this.LIST[i].format == 'INFO') {
412
        index.LIST = i;
413
        for (let j=0, subLen = this.LIST[i].subChunks.length; j < subLen; j++) {
414
          if (this.LIST[i].subChunks[j].chunkId == tag) {
415
            index.TAG = j;
416
            break;
417
          }
418
        }
419
        break;
420
      }
421
    }
422
    return index;
423
  }
424
425
  /**
426
   * Push a new cue point in this.cue.points.
427
   * @param {!Object} pointData A object with data of the cue point.
428
   * @param {number} dwName the dwName of the cue point
429
   * @private
430
   */
431
  setCuePoint_(pointData, dwName) {
432
    this.cue.points.push({
433
      dwName: dwName,
434
      dwPosition: pointData.dwPosition ? pointData.dwPosition : 0,
435
      fccChunk: pointData.fccChunk ? pointData.fccChunk : 'data',
436
      dwChunkStart: pointData.dwChunkStart ? pointData.dwChunkStart : 0,
437
      dwBlockStart: pointData.dwBlockStart ? pointData.dwBlockStart : 0,
438
      dwSampleOffset: pointData.dwSampleOffset
439
    });
440
    this.setLabl_(pointData, dwName);
441
  }
442
443
  /**
444
   * Push a new cue point in this.cue.points according to existing cue points.
445
   * @param {!Array} existingPoints Array with the existing points.
446
   * @param {!Object} pointData A object with data of the cue point.
447
   * @private
448
   */
449
  setCuePointInOrder_(existingPoints, pointData) {
450
    /** @type {boolean} */
451
    let hasSet = false;
452
453
    // Iterate over the cue points that existed
454
    // before this one was added
455
    for (let i = 0; i < existingPoints.length; i++) {
456
457
      // If the new point is located before this original point
458
      // and the new point have not been created, create the
459
      // new point and then the original point
460
      if (existingPoints[i].dwSampleOffset > 
461
        pointData.dwSampleOffset && !hasSet) {
462
        // create the new point
463
        this.setCuePoint_(pointData, i + 1);
464
465
        // create the original point
466
        this.setCuePoint_(existingPoints[i], i + 2);
467
        hasSet = true;
468
469
      // Otherwise, re-create the original point
470
      } else {
471
        this.setCuePoint_(existingPoints[i], hasSet ? i + 2 : i + 1);
472
      }
473
    }
474
    // If no point was created in the above loop,
475
    // create the new point as the last one
476
    if (!hasSet) {
477
      this.setCuePoint_(pointData, this.cue.points.length + 1);
478
    }
479
  }
480
481
  /**
482
   * Clear any LIST chunk labeled as 'adtl'.
483
   * @private
484
   */
485
  clearLISTadtl_() {
486
    for (let i = 0, len = this.LIST.length; i < len; i++) {
487
      if (this.LIST[i].format == 'adtl') {
488
        this.LIST.splice(i);
489
      }
490
    }
491
  }
492
493
  /**
494
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
495
   * This method creates a LIST adtl chunk in the file if one
496
   * is not present.
497
   * @param {!Object} pointData A object with data of the cue point.
498
   * @param {number} dwName The ID of the cue point.
499
   * @private
500
   */
501
  setLabl_(pointData, dwName) {
502
    /**
503
     * Get the index of the LIST chunk labeled as adtl.
504
     * A file can have many LIST chunks with unique labels.
505
     * @type {?number}
506
     */
507
    let adtlIndex = this.getAdtlChunk_();
508
    // If there is no adtl LIST, create one
509
    if (adtlIndex === null) {
510
      // Include a new item LIST chunk
511
      this.LIST.push({
512
        chunkId: 'LIST',
513
        chunkSize: 4,
514
        format: 'adtl',
515
        subChunks: []});
516
      // Get the index of the new LIST chunk
517
      adtlIndex = this.LIST.length - 1;
518
    }
519
    this.setLabelText_(adtlIndex, pointData, dwName);
520
    if (pointData.dwSampleLength) {
521
      this.setLtxtChunk_(adtlIndex, pointData, dwName);
522
    }
523
  }
524
525
  /**
526
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
527
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
528
   * @param {!Object} pointData A object with data of the cue point.
529
   * @param {number} dwName The ID of the cue point.
530
   * @private
531
   */
532
  setLabelText_(adtlIndex, pointData, dwName) {
533
    this.LIST[adtlIndex].subChunks.push({
534
      chunkId: 'labl',
535
      chunkSize: 4, // should be 4 + label length in bytes
536
      dwName: dwName,
537
      value: pointData.label
538
    });
539
    this.LIST[adtlIndex].chunkSize += 12; // should be 4 + label byte length
540
  }
541
  /**
542
   * Create a new 'ltxt' subchunk in a 'LIST' chunk of type 'adtl'.
543
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
544
   * @param {!Object} pointData A object with data of the cue point.
545
   * @param {number} dwName The ID of the cue point.
546
   * @private
547
   */
548
  setLtxtChunk_(adtlIndex, pointData, dwName) {
549
    this.LIST[adtlIndex].subChunks.push({
550
      chunkId: 'ltxt',
551
      chunkSize: 20,  // should be 12 + label byte length
552
      dwName: dwName,
553
      dwSampleLength: pointData.dwSampleLength,
554
      dwPurposeID: pointData.dwPurposeID ? pointData.dwPurposeID : 0,
555
      dwCountry: pointData.dwCountry ? pointData.dwCountry : 0,
556
      dwLanguage: pointData.dwLanguage ? pointData.dwLanguage : 0,
557
      dwDialect: pointData.dwDialect ? pointData.dwDialect : 0,
558
      dwCodePage: pointData.dwCodePage ? pointData.dwCodePage : 0,
559
      value: pointData.label // kept for compatibility
560
    });
561
    this.LIST[adtlIndex].chunkSize += 28;
562
  }
563
}
564
565
/**
566
 * Fix a RIFF tag format if possible, throw an error otherwise.
567
 * @param {string} tag The tag name.
568
 * @return {string} The tag name in proper fourCC format.
569
 * @private
570
 */
571
function fixRIFFTag_(tag) {
572
  if (tag.constructor !== String) {
573
    throw new Error('Invalid tag name.');
574
  } else if (tag.length < 4) {
575
    for (let i = 0, len = 4 - tag.length; i < len; i++) {
576
      tag += ' ';
577
    }
578
  }
579
  return tag;
580
}
581